﻿//************************************************************************************************************
// CoNatural.Data.dll
// Author: Roger Torres
//************************************************************************************************************
// You can use, modify, and distribute the source code and executable programs based on the source code.
// This source code is provided "as is" and without warranties as to performance or merchantability. 
// The author and/or distributors of this source code may have made statements about this source code. 
// Any such statements do not constitute warranties and shall not be relied on by the user in deciding 
// whether to use this source code. This source code is provided without any express or implied warranties 
// whatsoever. Because of the diversity of conditions and hardware under which this source code may be used, 
// no warranty of fitness for a particular purpose is offered. The user is advised to test the source code 
// thoroughly before relying on it. The user must assume the entire risk of using the source code.
//************************************************************************************************************

using System;
using System.Collections.Generic;
using System.Data;
#if NET40
using System.Linq.Expressions;
#else
using Microsoft.Scripting.Ast;
#endif
using CoNatural.Binders;

namespace CoNatural.Data.Binders {
	/// <summary>
	/// Parameter mapping
	/// </summary>
	public class ParameterMapping : MappingBase {
		public Parameter Parameter { get; private set; }

		protected internal ParameterMapping(string source, string target, Parameter parameter)
			: base(source, target, null) {
			Parameter = parameter;
		}
	}

	/// <summary>
	/// To be used by router when binding ICommand to IDbCommand
	/// </summary>
	public class InCommandBinderDelegateFactory : IBinderDelegateFactory {
		public IDbProvider DbProvider { get; private set; }

		public InCommandBinderDelegateFactory(IDbProvider dbProvider) {
			DbProvider = dbProvider;
		}

		#region IBinderDelegateFactory Members

		public IBinder CreateBinder(object source, Type targetType) {
			// source must implement ICommand
			var command = source as ICommand;
			if (command == null)
				throw new Exception("Source must implement ICommand");

			var commandType = source.GetType();
			var mappings = GetMappings(commandType);
			var sourceParam = Expression.Parameter(typeof(object), "source");
			var targetParam = Expression.Parameter(typeof(object), "target");
			var binderBlock = GetBinderBlock(commandType, sourceParam, targetParam, mappings);
			var mapperBlock = GetMapperBlock(mappings);
			return new BinderBase(
				Expression.Lambda<BinderDelegate>(binderBlock, sourceParam, targetParam).Compile(),
				Expression.Lambda<MapperDelegate>(mapperBlock, sourceParam).Compile()
			);
		}

		#endregion

		#region Binding

		/// <summary>
		/// Generates mappings from source to target type.
		/// </summary>
		/// <param name="commandType">The command type.</param>
		/// <returns>The mappings.</returns>
		private IList<ParameterMapping> GetMappings(Type commandType) {
			var mappings = new List<ParameterMapping>();

			// parameter mappings based on iCommand's public properties
			foreach (var p in Parameter.GetParameters(DbProvider, commandType))
				mappings.Add(new ParameterMapping(p.Name, p.Name, p));

			return mappings;
		}

		#endregion Binding

		#region Expressions

		private BlockExpression GetBinderBlock(Type commandType, ParameterExpression sourceParam, ParameterExpression targetParam, IList<ParameterMapping> mappings) {
			var end = Expression.Label(typeof(object), "end");
			var dbprov = Expression.Variable(typeof(IDbProvider), "dbp");
			var source = Expression.Variable(commandType, "s");
			var target = Expression.Variable(typeof(IDbCommand), "t");

			return Expression.Block(
				new[] { dbprov, source, target },
				Expression.Assign(dbprov, Expression.Convert(targetParam, typeof(IDbProvider))),
				Expression.Assign(source, Expression.Convert(sourceParam, commandType)),
				Expression.Assign(target, Expression.Call(dbprov, typeof(IDbProvider).GetMethod("CreateCommand"))),
				Expression.Block(GetBinderExpressions(mappings, dbprov, source, target, end))
			);
		}

		private IEnumerable<Expression> GetBinderExpressions(IList<ParameterMapping> mappings, Expression dbprov, Expression source, Expression target, LabelTarget end) {
			foreach (var mapping in mappings) {
				var p = mapping.Parameter;
				// create parameter and set input value from mapped property
				var targetparam = Expression.Call(
					dbprov, 
					typeof(IDbProvider).GetMethod("CreateDataParameter"),
 					Expression.Constant(p.Name, typeof(string)),
					Expression.Constant(p.DbType, typeof(DbType)),
					Expression.Constant(p.Direction, typeof(ParameterDirection)),
					Expression.Constant(p.Size, typeof(int)),
					Expression.Constant(p.Precision, typeof(byte)),
					Expression.Constant(p.Scale, typeof(byte)),
					Expression.Constant(p.IsNullable, typeof(bool)),
					Expression.Constant(p.UdtName, typeof(string)),
					p.Direction == ParameterDirection.Output ? (Expression)Expression.Constant(null) : Expression.Convert(Expression.Property(source, p.Name), typeof(object))
				);
				var targetparams = Expression.Property(target, typeof(IDbCommand).GetProperty("Parameters"));
				yield return Expression.Call(targetparams, typeof(System.Collections.IList).GetMethod("Add"), targetparam);
			}
			yield return Expression.Label(end, target);
		}

		private BlockExpression GetMapperBlock(IList<ParameterMapping> mappings) {
			var maplist = Expression.Variable(typeof(List<IMapping>), "mappings");
			var end = Expression.Label(typeof(List<IMapping>), "end");

			return Expression.Block(
				new[] { maplist },
				Expression.Assign(maplist, Expression.New(typeof(List<IMapping>))),
				Expression.Block(GetMapperExpressions(mappings, maplist, end))
			);
		}

		private IEnumerable<Expression> GetMapperExpressions(IList<ParameterMapping> mappings, Expression maplist, LabelTarget end) {
			foreach (var mapping in mappings) {
				// create expression to construct new IMapping from mapping
				var constructor = typeof(MappingBase).GetConstructor(new[] { typeof(string), typeof(string), typeof(object) });
				var map = Expression.New(
					constructor,
					Expression.Constant(mapping.Source),
					Expression.Constant(mapping.Target),
					Expression.Constant(null)
				);
				yield return Expression.Call(maplist, typeof(List<IMapping>).GetMethod("Add"), map);
			}
			yield return Expression.Label(end, maplist);
		}

		#endregion Expressions
	}

	/// <summary>
	/// To be used by router when binding IDbCommand to ICommand
	/// </summary>
	public class OutCommandBinderDelegateFactory : IBinderDelegateFactory {
		public IDbProvider DbProvider { get; private set; }

		public OutCommandBinderDelegateFactory(IDbProvider dbProvider) {
			DbProvider = dbProvider;
		}

		#region IBinderDelegateFactory Members

		public IBinder CreateBinder(object source, Type targetType) {
			// target must implement ICommand
			if (targetType.GetInterface(typeof(ICommand).Name) == null)
				throw new Exception("Target must implement ICommand");

			var mappings = GetMappings(targetType);
			var sourceParam = Expression.Parameter(typeof(object), "source");
			var targetParam = Expression.Parameter(typeof(object), "target");
			var binderBlock = GetBinderBlock(targetType, sourceParam, targetParam, mappings);
			var mapperBlock = GetMapperBlock(mappings);
			return new BinderBase(
				Expression.Lambda<BinderDelegate>(binderBlock, sourceParam, targetParam).Compile(),
				Expression.Lambda<MapperDelegate>(mapperBlock, sourceParam).Compile()
			);
		}

		#endregion

		#region Binding

		/// <summary>
		/// Generates mappings from source to target type.
		/// </summary>
		/// <param name="commandType">The command type.</param>
		/// <returns>The mappings.</returns>
		private IList<ParameterMapping> GetMappings(Type commandType) {
			var mappings = new List<ParameterMapping>();

			// parameter mappings based on iCommand's public properties
			foreach (var p in Parameter.GetParameters(DbProvider, commandType))
				if(p.Direction != ParameterDirection.Input) // just map output parameters
					mappings.Add(new ParameterMapping(p.Name, p.Name, p));

			return mappings;
		}

		#endregion Binding

		#region Expressions

		private BlockExpression GetBinderBlock(Type commandType, ParameterExpression sourceParam, ParameterExpression targetParam, IList<ParameterMapping> mappings) {
			var end = Expression.Label(typeof(object), "end");
			var source = Expression.Variable(typeof(IDbCommand), "s");
			var target = Expression.Variable(commandType, "t");

			return Expression.Block(
				new[] { source, target },
				Expression.Assign(source, Expression.Convert(sourceParam, typeof(IDbCommand))),
				Expression.Assign(target, Expression.Convert(targetParam, commandType)),
				Expression.Block(GetBinderExpressions(mappings, source, target, end))
			);
		}

		private IEnumerable<Expression> GetBinderExpressions(IList<ParameterMapping> mappings, Expression source, Expression target, LabelTarget end) {
			foreach (var mapping in mappings) {
				var p = mapping.Parameter;
				// set property from output parameter value
				var parameters = Expression.Property(source, typeof(IDbCommand).GetProperty("Parameters"));
				var itemprop = typeof(IDataParameterCollection).GetProperty("Item", new[] { typeof(string) });
				var targetparam = Expression.Convert(Expression.Property(parameters, itemprop, Expression.Constant(p.Name)), typeof(IDbDataParameter));
				var getter = Expression.Convert(Expression.Call(typeof(Parameter).GetMethod("GetValue"), targetparam), p.Type);
				yield return Expression.Assign(Expression.Property(target, p.Name), getter);
			}
			yield return Expression.Label(end, target);
		}

		private BlockExpression GetMapperBlock(IList<ParameterMapping> mappings) {
			var maplist = Expression.Variable(typeof(List<IMapping>), "mappings");
			var end = Expression.Label(typeof(List<IMapping>), "end");

			return Expression.Block(
				new[] { maplist },
				Expression.Assign(maplist, Expression.New(typeof(List<IMapping>))),
				Expression.Block(GetMapperExpressions(mappings, maplist, end))
			);
		}

		private IEnumerable<Expression> GetMapperExpressions(IList<ParameterMapping> mappings, Expression maplist, LabelTarget end) {
			foreach (var mapping in mappings) {
				// create expression to construct new IMapping from mapping
				var constructor = typeof(MappingBase).GetConstructor(new[] { typeof(string), typeof(string), typeof(object) });
				var map = Expression.New(
					constructor,
					Expression.Constant(mapping.Source),
					Expression.Constant(mapping.Target),
					Expression.Constant(null)
				);
				yield return Expression.Call(maplist, typeof(List<IMapping>).GetMethod("Add"), map);
			}
			yield return Expression.Label(end, maplist);
		}

		#endregion Expressions
	}
}
